package net.capstone.cra.smartcontroller;

import java.util.Timer;
import java.util.TimerTask;

import net.capstone.cra.smartcontroller.filter.HighPassFilter;
import net.capstone.cra.smartcontroller.filter.LowPassFilter;
import net.capstone.cra.smartcontroller.filter.SensorFilter;
import net.capstone.cra.smartcontroller.motion.AccelerometerEvent;
import net.capstone.cra.smartcontroller.motion.Motion;
import net.capstone.cra.smartcontroller.motion.MotionDetector;
import net.capstone.cra.smartcontroller.util.ByteConverter;
import android.app.Activity;
import android.app.AlertDialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Paint;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.opengl.Matrix;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Vibrator;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.Window;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;

/**
 * The Class MainActivity.
 */
public class MainActivity extends Activity {

	/** The Constant TAG. */
	private static final String TAG = "SmartController";

	/** The Constant D. */
	private static final boolean D = false;

	// Message types sent from the BluetoothService Handler
	/** The Constant MESSAGE_STATE_CHANGE. */
	public static final int MESSAGE_STATE_CHANGE = 1;

	/** The Constant MESSAGE_SIGNAL. */
	public static final int MESSAGE_SIGNAL = 2;

	/** The Constant MESSAGE_CONNECTED. */
	public static final int MESSAGE_CONNECTED = 3;

	/** The Constant MESSAGE_TOAST. */
	public static final int MESSAGE_TOAST = 4;

	// Key names received from the BluetoothService Handler
	/** Key names received from the BluetoothService Handler. */
	public static final String DEVICE_NAME = "device_name";

	/** The Constant TOAST. */
	public static final String TOAST = "toast";

	// Intent request codes
	/** The Constant REQUEST_CONNECT_DEVICE. */
	private static final int REQUEST_CONNECT_DEVICE = 1;

	/** The Constant REQUEST_ENABLE_BT. */
	private static final int REQUEST_ENABLE_BT = 2;

	/** The Constant REPORT_SIZE. */
	private static final int REPORT_SIZE = 62;

	// Layout Views
	/** The UI title. */
	private TextView mTitle;

	/** The controller no. */
	private TextView controllerNo;

	/** The button a(Left button). */
	private ImageButton btnA;

	/** The button b(Right button). */
	private ImageButton btnB;

	/** Local Bluetooth adapter. */
	private BluetoothAdapter mBluetoothAdapter = null;

	/** Member object for the services. */
	private BluetoothService mBthService = null;

	/** The sensor manager. */
	private SensorManager sensorManager;

	/** The accelerometer sensor. */
	private Sensor accSensor;
	
	/** The linear acceleration sensor. */
	private Sensor linearAccSensor;

	/** The magnetic sensor. */
	private Sensor magSensor;
	

	/** The accelerometer value. */
	private float[] accValue = new float[3];
	
	/** 
	 * The linear acceleration value. 
	 * Same as Accelerometer's value with gravity factor canceled out.
	 */
	private float[] linearAccValue = new float[3];

	/** The magnetic value. */
	private float[] magValue = new float[3];

	/** The motion value. */
	private Byte motionValue = 0x0;

	/** The accelerometer sensor handler. */
	private AccelHandler accHandler;
	
	private LinearAccelHandler linearAccHandler;

	/** The magnetic sensor handler. */
	private MagHandler magHandler;

	/** The timer used when sending the report periodically. */
	private Timer reportTimer;

	/**
	 * The flag indicating that the value used to calibrate orientation.
	 */
	/** should be updated. */
	private boolean calibrateOrient = false;

	/** The calibration offset. */
	private float orientOffset = 0.0f;
	
	private SensorFilter gravityFilter = new HighPassFilter();

	// The Handler that gets information back from the BluetoothService
	/** The message handler. */
	private final Handler mHandler = new Handler() {
		@Override
		public void handleMessage(Message msg) {
			switch (msg.what) {
			case MESSAGE_STATE_CHANGE:
				if (D)
					Log.i(TAG, "MESSAGE_STATE_CHANGE: " + msg.arg1);
				switch (msg.arg1) {
				case BluetoothService.STATE_CONNECTED:
					mTitle.setText(R.string.title_connected_to);
					break;
				case BluetoothService.STATE_CONNECTING:
					mTitle.setText(R.string.title_connecting);
					break;
				case BluetoothService.STATE_NONE:
					mTitle.setText(R.string.title_not_connected);
					controllerNo.setText("");
					break;
				}
				break;
			case MESSAGE_SIGNAL:
				byte[] readBuf = (byte[]) msg.obj;
				processSignal(SignalDecoder.decode(readBuf));
				break;
			case MESSAGE_CONNECTED:
				// Notify user that the controller is connected.
				notifyConnected();

				// Calibrate the orientation for the first time
				calibrateOrient();
				break;
			case MESSAGE_TOAST:
				Toast.makeText(getApplicationContext(),
						msg.getData().getString(TOAST), Toast.LENGTH_SHORT)
						.show();
				break;
			}
		}
	};

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.app.Activity#onCreate(android.os.Bundle)
	 */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		initUI();

		initBth();

		initSensor();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.app.Activity#onStart()
	 */
	@Override
	public void onStart() {
		super.onStart();

		if (D)
			Log.e(TAG, "++ ON START ++");

		// If BT is not on, request that it be enabled.
		// setup() will then be called during onActivityResult
		if (!mBluetoothAdapter.isEnabled()) {
			Intent enableIntent = new Intent(
					BluetoothAdapter.ACTION_REQUEST_ENABLE);
			startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
		} else {
			// Otherwise, setup the session
			if (mBthService == null) {
				setup();
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.app.Activity#onResume()
	 */
	@Override
	public synchronized void onResume() {
		super.onResume();
		if (D)
			Log.e(TAG, "+ ON RESUME +");

		// Performing this check in onResume() covers the case in which BT was
		// not enabled during onStart(), so we were paused to enable it...
		// onResume() will be called when ACTION_REQUEST_ENABLE activity
		// returns.
		if (mBthService != null) {
			// Only if the state is STATE_NONE, do we know that we haven't
			// started already
			if (mBthService.getState() == BluetoothService.STATE_NONE) {
				// Start the Bluetooth services
				mBthService.start();

				startReportService();
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.app.Activity#onPause()
	 */
	@Override
	public synchronized void onPause() {
		super.onPause();
		if (D)
			Log.e(TAG, "- ON PAUSE -");
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.app.Activity#onStop()
	 */
	@Override
	public void onStop() {
		super.onStop();

		stopReportService();

		if (D)
			Log.e(TAG, "-- ON STOP --");
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.app.Activity#onDestroy()
	 */
	@Override
	public void onDestroy() {
		super.onDestroy();
		// Stop the Bluetooth services
		if (mBthService != null) {
			mBthService.stop();
		}
		if (D)
			Log.e(TAG, "--- ON DESTROY ---");
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.app.Activity#finish()
	 */
	@Override
	public void finish() {
		stopReportService();

		super.finish();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.app.Activity#onActivityResult(int, int,
	 * android.content.Intent)
	 */
	@Override
	public void onActivityResult(int requestCode, int resultCode, Intent data) {
		if (D)
			Log.d(TAG, "onActivityResult " + resultCode);
		switch (requestCode) {
		case REQUEST_CONNECT_DEVICE:
			// When DeviceListActivity returns with a device to connect
			if (resultCode == Activity.RESULT_OK) {
				// Get the device MAC address
				String address = data.getExtras().getString(
						DeviceListActivity.EXTRA_DEVICE_ADDRESS);
				// Get the BLuetoothDevice object
				BluetoothDevice device = mBluetoothAdapter
						.getRemoteDevice(address);
				// Attempt to connect to the device
				mBthService.connect(device);
			}
			break;
		case REQUEST_ENABLE_BT:
			// When the request to enable Bluetooth returns
			if (resultCode == Activity.RESULT_OK) {
				// Bluetooth is now enabled, so set up a session
				setup();
			} else {
				// User did not enable Bluetooth or an error occured
				Log.d(TAG, "BT not enabled");

				Toast.makeText(this, R.string.bt_not_enabled_leaving,
						Toast.LENGTH_SHORT).show();

				// Stop the controller service and dispose
				finish();
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.app.Activity#onCreateOptionsMenu(android.view.Menu)
	 */
	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		MenuInflater inflater = getMenuInflater();
		inflater.inflate(R.menu.option_menu, menu);
		return true;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.app.Activity#onOptionsItemSelected(android.view.MenuItem)
	 */
	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		switch (item.getItemId()) {
		case R.id.scan:
			// Launch the DeviceListActivity to see devices and do scan
			Intent serverIntent = new Intent(this, DeviceListActivity.class);
			startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE);
			return true;
		case R.id.calibrate_orientation:
			calibrateOrient();
			return true;
		case R.id.about:
			AlertDialog.Builder b = new AlertDialog.Builder(this);
			b.setTitle(R.string.app_name);
			b.setMessage(R.string.maker);
			
			b.setIcon(R.drawable.icon);
					
			b.setPositiveButton(R.string.close, new DialogInterface.OnClickListener() {
				@Override
				public void onClick(DialogInterface dialog, int which) {
					// TODO Auto-generated method stub
					dialog.dismiss();
				}
			});
			b.show();
		}
		return false;
	}

	/**
	 * Bluetooth connection initialize.
	 */
	private void setup() {
		Log.d(TAG, "setup()");
	
		// Initialize the BluetoothService to perform bluetooth connections
		mBthService = new BluetoothService(this, mHandler);
	}

	/**
	 * Initializes the UI.
	 */
	private void initUI() {
		// Set up the window layout
		requestWindowFeature(Window.FEATURE_CUSTOM_TITLE);
		setContentView(R.layout.main);
		getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE,
				R.layout.custom_title);

		setContentView(R.layout.main);

		btnA = (ImageButton) findViewById(R.id.btnA);
		btnB = (ImageButton) findViewById(R.id.btnB);

		// Set up the custom title
		mTitle = (TextView) findViewById(R.id.title_left_text);
		mTitle.setText(R.string.app_name);
		mTitle = (TextView) findViewById(R.id.title_right_text);

		controllerNo = (TextView) findViewById(R.id.ctrlNumText);
		controllerNo.setPaintFlags(controllerNo.getPaintFlags()
				| Paint.FAKE_BOLD_TEXT_FLAG);
	}

	/**
	 * Initialize Sensor Manager and Sensors.
	 */
	private void initBth() {
		// Get local Bluetooth adapter
		mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

		// If the adapter is null, then Bluetooth is not supported
		if (mBluetoothAdapter == null) {
			Toast.makeText(this, "Bluetooth is not available",
					Toast.LENGTH_LONG).show();
			finish();
			return;
		}
	}

	/**
	 * Initialize Sensor Manager and Sensors.
	 */
	private void initSensor() {
		// Initialize Sensors
		sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
		accSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
		magSensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
		//linearAccSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION);
	}

	/**
	 * Register sensor handler.
	 */
	private void registerSensorHandler() {
		// Make sure that previous handlers are disabled
		unregisterSensorHandler();
	
		// instantiate the Sensor handlers
		accHandler = new AccelHandler();
		magHandler = new MagHandler();
		linearAccHandler = new LinearAccelHandler();
	
		// register the handlers
		sensorManager.registerListener(accHandler, accSensor,
				SensorManager.SENSOR_DELAY_FASTEST);
		sensorManager.registerListener(magHandler, magSensor,
				SensorManager.SENSOR_DELAY_FASTEST);
		sensorManager.registerListener(linearAccHandler, linearAccSensor,
				SensorManager.SENSOR_DELAY_FASTEST);
	}

	/**
	 * Unregister sensor handler.
	 */
	private void unregisterSensorHandler() {
		sensorManager.unregisterListener(accHandler);
		sensorManager.unregisterListener(magHandler);
		sensorManager.unregisterListener(linearAccHandler);
	}

	/**
	 * Registers Sensor Handlers, and starts sending data via bluetooth.
	 */
	private void startReportService() {
		registerSensorHandler();
		reportTimer = new Timer("SendSignal");
		reportTimer.scheduleAtFixedRate(new ReportSendTask(), 0, 10);
	}

	/**
	 * Unregister Sensor Handlers, and stop sending data.
	 */
	private void stopReportService() {
		unregisterSensorHandler();
		
		if (reportTimer != null) {
			reportTimer.cancel();
		}
	}

	/**
	 * Builds the report.
	 * 
	 * @return the byte[]
	 */
	private byte[] buildReport() {
		// get rotation matrix
		float[] rotMat = getRotationMatrix(accValue, magValue);

		// get euler
		float[] euler = new float[3];
		SensorManager.getOrientation(rotMat, euler);

		// get button values
		boolean A = btnA.isPressed();
		boolean B = btnB.isPressed();

		byte btnValue = 0;
		if (A) {
			btnValue = (byte) (btnValue | 0x1);
		}
		if (B) {
			btnValue = (byte) (btnValue | 0x2);
		}

		// convert values into byte array
		byte[][] array = new byte[15][4];

		// encode Accelerometer values
		// use linear acceleration if possible
		float[] accValue = linearAccSensor == null ? gravityFilter.filter(this.accValue) : this.linearAccValue; 
		
		array[0] = ByteConverter.float2bytes(accValue[0]);
		array[1] = ByteConverter.float2bytes(accValue[1]);
		array[2] = ByteConverter.float2bytes(accValue[2]);

		// encode rotation matrix
		array[3] = ByteConverter.float2bytes(rotMat[0]);
		array[4] = ByteConverter.float2bytes(rotMat[1]);
		array[5] = ByteConverter.float2bytes(rotMat[2]);
		array[6] = ByteConverter.float2bytes(rotMat[4]);
		array[7] = ByteConverter.float2bytes(rotMat[5]);
		array[8] = ByteConverter.float2bytes(rotMat[6]);
		array[9] = ByteConverter.float2bytes(rotMat[8]);
		array[10] = ByteConverter.float2bytes(rotMat[9]);
		array[11] = ByteConverter.float2bytes(rotMat[10]);

		// encode euler angle
		array[12] = ByteConverter.float2bytes(euler[0]);
		array[13] = ByteConverter.float2bytes(euler[1]);
		array[14] = ByteConverter.float2bytes(euler[2]);

		// flatten the values into 1-dimension byte array
		byte[] report = new byte[REPORT_SIZE];
		int len = 0;

		for (int i = 0; i < array.length; i++) {
			for (int j = 0; j < 4; j++) {
				report[len + j] = array[i][j];
			}
			len += array[i].length;
		}

		// put button values
		report[REPORT_SIZE - 2] = btnValue;

		// put motion values
		report[REPORT_SIZE - 1] = motionValue;
		synchronized (motionValue) {
			motionValue = Motion.TYPE_NONE; // reset the motion after sending it
		}

		// return the encoded report
		return report;
	}

	/**
	 * Sends a report via Bluetooth service.
	 * 
	 * @param report
	 *            A string of text to send.
	 */
	private void sendReport(byte[] report) {
		// Check that there's actually something to send
		if (mBthService.getState() == BluetoothService.STATE_CONNECTED) {
			// Get the message bytes and tell the BluetoothService to write
			mBthService.write(report);
		}
	}

	/**
	 * Process signal. do something according to the received signal
	 * 
	 * @param sig
	 *            the signal sent from the server
	 */
	private void processSignal(Signal sig) {
		switch (sig.type) {
		case Signal.SIG_CONTROLLERNUM:
			controllerNo.setText(Integer.toString(sig.value + 1) + "P");
			break;
		case Signal.SIG_VIBRATION:
			vibrate(sig.value);
			break;
		default:
			break;
		}
	}

	/**
	 * Calibrate orient.
	 */
	private void calibrateOrient() {
		calibrateOrient = true;
	}

	/**
	 * Calculate the rotation matrix using Android API and do some additional
	 * calculation.
	 * 
	 * @param accValue
	 *            the acc value
	 * @param magValue
	 *            the mag value
	 * @return the rotation matrix
	 */
	private float[] getRotationMatrix(float[] accValue, float[] magValue) {
		float[] R = new float[16];
		float[] euler = new float[3];

		// get the rotation matrix and euler angles
		SensorManager.getRotationMatrix(R, null, accValue, magValue);
		SensorManager.getOrientation(R, euler);

		// if the calibration flag is set, update the calibration
		// value.
		if (calibrateOrient) {
			orientOffset = euler[0];
			calibrateOrient = false;
		}

		// 컨트롤러를 들고 있는 방향이 north pole이 아닌 이상 Z축에 대한 초기 회전값은 0이 아니다. 
		// 따라서 들고 있는 방향에 맞춰 자기장 센서 방향 보정 (초기값 만큼 거꾸로 회전)
		Matrix.rotateM(R, 0, -(float) Math.toDegrees(orientOffset), 0, 0, 1.0f);
		
		// OpenGL의 행렬 형식에 맞추기 위해 회전 행렬을 invert 시킴
		// row major -> column major
		float[] transR = new float[16]; 
		Matrix.transposeM(transR, 0, R, 0);
		
		// 센서 좌표계를 OpenGL 좌표계에 맞추기 위해 remap 수행
		float[] remapR = new float[16];
		SensorManager.remapCoordinateSystem(transR, SensorManager.AXIS_X, SensorManager.AXIS_MINUS_Z, remapR);
		
		// 각종 변환을 거친 최종적인 회전 행렬
		float[] finalR = remapR;

		return finalR;
	}

	/**
	 * Toast message.
	 * 
	 * @param message
	 *            the message
	 */
	private void toastMessage(String message) {
		Message msg = mHandler.obtainMessage(MainActivity.MESSAGE_TOAST);
		Bundle bundle = new Bundle();
		bundle.putString(MainActivity.TOAST, message);
		msg.setData(bundle);
		mHandler.sendMessage(msg);
	}

	/**
	 * Notify if connected.
	 */
	private void notifyConnected() {
		vibrate(800);
	}

	/**
	 * Vibrate.
	 * 
	 * @param milliseconds
	 *            the milliseconds
	 */
	private void vibrate(long milliseconds) {
		Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);

		if (milliseconds < 0) {
			milliseconds = 800;
		}
		vibrator.vibrate(milliseconds);
	}

	/**
	 * The Class AccelHandler.
	 */
	private class AccelHandler implements SensorEventListener {

		/** The motion detector. */
		private MotionDetector motionDetector = new MotionDetector();

		/** The sensor filter. */
		private SensorFilter sensorFilter = new LowPassFilter();

		/** The shake count. */
		private int shakeCount = 0;

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * android.hardware.SensorEventListener#onAccuracyChanged(android.hardware
		 * .Sensor, int)
		 */
		@Override
		public void onAccuracyChanged(Sensor sensor, int accuracy) {
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * android.hardware.SensorEventListener#onSensorChanged(android.hardware
		 * .SensorEvent)
		 */
		@Override
		public void onSensorChanged(SensorEvent event) {
			accValue = event.values;

			// detect motion
			handleMotion(event);
		}

		/**
		 * Handle motion.
		 * 
		 * @param event
		 *            the event
		 */
		private void handleMotion(SensorEvent event) {
			float[] valueFiltered = sensorFilter.filter(event.values);
			AccelerometerEvent aEvent = new AccelerometerEvent(
					valueFiltered[0], valueFiltered[1], valueFiltered[2],
					event.timestamp);

			// 모션 발생 시, 모션 종류 감지 후 신호 전송
			int motionType = motionDetector.detect(aEvent);

			// 모션이 인식 되었을 때만 notify
			if (motionType != Motion.TYPE_NONE) {
				notifyMotion(motionType);
			}
		}

		/**
		 * Notify motion.
		 * 
		 * @param motionType
		 *            the motion type
		 */
		private void notifyMotion(int motionType) {
			synchronized (motionValue) {
				motionValue = (byte) motionType;
			}

			if (D) {
				switch (motionType) {
				case Motion.TYPE_SHAKE:
					shakeCount++;
					toastMessage("" + shakeCount);
					break;
				case Motion.TYPE_NONE:
					break;
				default:
					break;
				}
			}
		}
	}
	
	/**
	 * The Class LinearAccelHandler.
	 */
	private class LinearAccelHandler implements SensorEventListener {

		/* (non-Javadoc)
		 * @see android.hardware.SensorEventListener#onAccuracyChanged(android.hardware.Sensor, int)
		 */
		@Override
		public void onAccuracyChanged(Sensor sensor, int accuracy) {
			
		}

		/* (non-Javadoc)
		 * @see android.hardware.SensorEventListener#onSensorChanged(android.hardware.SensorEvent)
		 */
		@Override
		public void onSensorChanged(SensorEvent event) {
			linearAccValue = event.values;
		}
		
	}

	/**
	 * The Magnetic sensor Handler class.
	 */
	private class MagHandler implements SensorEventListener {

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * android.hardware.SensorEventListener#onAccuracyChanged(android.hardware
		 * .Sensor, int)
		 */
		@Override
		public void onAccuracyChanged(Sensor sensor, int accuracy) {

		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * android.hardware.SensorEventListener#onSensorChanged(android.hardware
		 * .SensorEvent)
		 */
		@Override
		public void onSensorChanged(SensorEvent event) {
			magValue = event.values;
		}

	}

	/**
	 * The Report Send Task Class.
	 */
	private class ReportSendTask extends TimerTask {

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.util.TimerTask#run()
		 */
		@Override
		public void run() {
			byte[] report = buildReport();

			if (report != null) {
				sendReport(report);
			}
		}
	}
}
